//
//  ========================================================================
//  Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package com.foreveross.project.projectonline.web.filter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationListener;
import org.eclipse.jetty.continuation.ContinuationSupport;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.http.gzip.AbstractCompressedStream;
import org.eclipse.jetty.http.gzip.CompressedResponseWrapper;
import org.eclipse.jetty.servlets.UserAgentFilter;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

import com.foreveross.project.projectonline.jfinal.plugin.CacheKit;
import com.foreveross.project.projectonline.web.cache.DefaultPathCache;
import com.foreveross.project.projectonline.web.cache.PathCache;

/* ------------------------------------------------------------ */
/** GZIP Filter
 * This filter will gzip or deflate the content of a response if: <ul>
 * <li>The filter is mapped to a matching path</li>
 * <li>accept-encoding header is set to either gzip, deflate or a combination of those</li>
 * <li>The response status code is >=200 and <300
 * <li>The content length is unknown or more than the <code>minGzipSize</code> initParameter or the minGzipSize is 0(default)</li>
 * <li>The content-type is in the comma separated list of mimeTypes set in the <code>mimeTypes</code> initParameter or
 * if no mimeTypes are defined the content-type is not "application/gzip"</li>
 * <li>No content-encoding is specified by the resource</li>
 * </ul>
 * 
 * <p>
 * If both gzip and deflate are specified in the accept-encoding header, then gzip will be used.
 * </p>
 * <p>
 * Compressing the content can greatly improve the network bandwidth usage, but at a cost of memory and
 * CPU cycles. If this filter is mapped for static content, then use of efficient direct NIO may be 
 * prevented, thus use of the gzip mechanism of the {@link org.eclipse.jetty.servlet.DefaultServlet} is 
 * advised instead.
 * </p>
 * <p>
 * This filter extends {@link UserAgentFilter} and if the the initParameter <code>excludedAgents</code> 
 * is set to a comma separated list of user agents, then these agents will be excluded from gzip content.
 * </p>
 * <p>Init Parameters:</p>
 * <PRE>
 * bufferSize                 The output buffer size. Defaults to 8192. Be careful as values <= 0 will lead to an 
 *                            {@link IllegalArgumentException}. 
 *                            See: {@link java.util.zip.GZIPOutputStream#GZIPOutputStream(java.io.OutputStream, int)}
 *                            and: {@link java.util.zip.DeflaterOutputStream#DeflaterOutputStream(java.io.OutputStream, Deflater, int)}
 *                      
 * minGzipSize                Content will only be compressed if content length is either unknown or greater
 *                            than <code>minGzipSize</code>.
 *                      
 * deflateCompressionLevel    The compression level used for deflate compression. (0-9).
 *                            See: {@link java.util.zip.Deflater#Deflater(int, boolean)}
 *                            
 * deflateNoWrap              The noWrap setting for deflate compression. Defaults to true. (true/false)
 *                            See: {@link java.util.zip.Deflater#Deflater(int, boolean)}
 *
 * mimeTypes                  Comma separated list of mime types to compress. See description above.
 * 
 * excludedAgents             Comma separated list of user agents to exclude from compression. Does a 
 *                            {@link String#contains(CharSequence)} to check if the excluded agent occurs
 *                            in the user-agent header. If it does -> no compression
 *                            
 * excludeAgentPatterns       Same as excludedAgents, but accepts regex patterns for more complex matching.
 * 
 * excludePaths               Comma separated list of paths to exclude from compression. 
 *                            Does a {@link String#startsWith(String)} comparison to check if the path matches.
 *                            If it does match -> no compression. To match subpaths use <code>excludePathPatterns</code>
 *                            instead.
 * 
 * excludePathPatterns        Same as excludePath, but accepts regex patterns for more complex matching.
 * </PRE>
 */
public class GzipFilter extends UserAgentFilter {
	private static final Logger LOG = Log.getLogger(GzipFilter.class);
	public final static String GZIP = "gzip";
	public final static String DEFLATE = "deflate";

	protected Set<String> _mimeTypes;
	protected int _bufferSize = 8192;
	protected int _minGzipSize = 256;
	protected int _deflateCompressionLevel = Deflater.DEFAULT_COMPRESSION;
	protected boolean _deflateNoWrap = true;
	protected Set<String> _excludedAgents;
	protected Set<Pattern> _excludedAgentPatterns;
	protected Set<String> _excludedPaths;
	protected Set<Pattern> _excludedPathPatterns;

	private static final int STATE_SEPARATOR = 0;
	private static final int STATE_Q = 1;
	private static final int STATE_QVALUE = 2;
	private static final int STATE_DEFAULT = 3;

	//tyler chen
	protected PathCache pathCache = DefaultPathCache.me();

	/* ------------------------------------------------------------ */
	/**
	 * @see org.eclipse.jetty.servlets.UserAgentFilter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		super.init(filterConfig);

		String tmp = filterConfig.getInitParameter("bufferSize");
		if (tmp != null)
			_bufferSize = Integer.parseInt(tmp);

		tmp = filterConfig.getInitParameter("minGzipSize");
		if (tmp != null)
			_minGzipSize = Integer.parseInt(tmp);

		tmp = filterConfig.getInitParameter("deflateCompressionLevel");
		if (tmp != null)
			_deflateCompressionLevel = Integer.parseInt(tmp);

		tmp = filterConfig.getInitParameter("deflateNoWrap");
		if (tmp != null)
			_deflateNoWrap = Boolean.parseBoolean(tmp);

		tmp = filterConfig.getInitParameter("mimeTypes");
		if (tmp != null) {
			_mimeTypes = new HashSet<String>();
			StringTokenizer tok = new StringTokenizer(tmp, ",", false);
			while (tok.hasMoreTokens())
				_mimeTypes.add(tok.nextToken());
		}
		tmp = filterConfig.getInitParameter("excludedAgents");
		if (tmp != null) {
			_excludedAgents = new HashSet<String>();
			StringTokenizer tok = new StringTokenizer(tmp, ",", false);
			while (tok.hasMoreTokens())
				_excludedAgents.add(tok.nextToken());
		}

		tmp = filterConfig.getInitParameter("excludeAgentPatterns");
		if (tmp != null) {
			_excludedAgentPatterns = new HashSet<Pattern>();
			StringTokenizer tok = new StringTokenizer(tmp, ",", false);
			while (tok.hasMoreTokens())
				_excludedAgentPatterns.add(Pattern.compile(tok.nextToken()));
		}

		tmp = filterConfig.getInitParameter("excludePaths");
		if (tmp != null) {
			_excludedPaths = new HashSet<String>();
			StringTokenizer tok = new StringTokenizer(tmp, ",", false);
			while (tok.hasMoreTokens())
				_excludedPaths.add(tok.nextToken());
		}

		tmp = filterConfig.getInitParameter("excludePathPatterns");
		if (tmp != null) {
			_excludedPathPatterns = new HashSet<Pattern>();
			StringTokenizer tok = new StringTokenizer(tmp, ",", false);
			while (tok.hasMoreTokens())
				_excludedPathPatterns.add(Pattern.compile(tok.nextToken()));
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * @see org.eclipse.jetty.servlets.UserAgentFilter#destroy()
	 */
	@Override
	public void destroy() {
	}

	/* ------------------------------------------------------------ */
	/**
	 * @see org.eclipse.jetty.servlets.UserAgentFilter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
			ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		// Inform caches that responses may vary according to Accept-Encoding
		response.setHeader("Vary", "Accept-Encoding");

		// Should we vary this response according to Accept-Encoding
		String compressionType = selectCompression(request.getHeader("accept-encoding"));
		if (compressionType != null && !response.containsHeader("Content-Encoding")
				&& !HttpMethods.HEAD.equalsIgnoreCase(request.getMethod())) {
			String ua = getUserAgent(request);
			if (isExcludedAgent(ua)) {
				super.doFilter(request, response, chain);
				return;
			}
			String requestURI = request.getRequestURI();
			if (isExcludedPath(requestURI)) {
				super.doFilter(request, response, chain);
				return;
			}
			String servletPath = request.getServletPath();
			String queryString = request.getQueryString();
			String cacheKey = pathCache.cacheKey(servletPath, queryString);
			if (cacheKey != null) {
				request.setAttribute("_TYLER_CACHE_", cacheKey);
				Object object = CacheKit.get(cacheKey);
				if (object != null && object instanceof byte[]) {
					System.out.println("===TOUCH CACHE===" + cacheKey);
					byte[] bs = (byte[]) object;
					response.setIntHeader("Content-Length", bs.length);
					response.setHeader("Content-Type", "text/html;charset=UTF-8");
					response.setHeader("Content-Encoding", compressionType);
					response.setHeader("Transfer-Encoding", "chunked");
					response.setHeader("Vary", "Accept-Encoding");
					response.getOutputStream().write(bs);
					response.getOutputStream().flush();
					response.getOutputStream().close();
					return;
				}
			}
			CompressedResponseWrapper wrappedResponse = createWrappedResponse(request, response, compressionType);

			boolean exceptional = true;
			try {
				super.doFilter(request, wrappedResponse, chain);
				exceptional = false;
			} finally {
				Continuation continuation = ContinuationSupport.getContinuation(request);
				if (continuation.isSuspended() && continuation.isResponseWrapped()) {
					continuation.addContinuationListener(new ContinuationListenerWaitingForWrappedResponseToFinish(
							wrappedResponse));
				} else if (exceptional && !response.isCommitted()) {
					wrappedResponse.resetBuffer();
					wrappedResponse.noCompression();
				} else
					wrappedResponse.finish();
			}
		} else {
			super.doFilter(request, response, chain);
		}
	}

	/* ------------------------------------------------------------ */
	private String selectCompression(String encodingHeader) {
		// TODO, this could be a little more robust.
		// prefer gzip over deflate
		String compression = null;
		if (encodingHeader != null) {

			String[] encodings = getEncodings(encodingHeader);
			if (encodings != null) {
				for (int i = 0; i < encodings.length; i++) {
					if (encodings[i].toLowerCase(Locale.ENGLISH).contains(GZIP)) {
						if (isEncodingAcceptable(encodings[i])) {
							compression = GZIP;
							break; //prefer Gzip over deflate
						}
					}

					if (encodings[i].toLowerCase(Locale.ENGLISH).contains(DEFLATE)) {
						if (isEncodingAcceptable(encodings[i])) {
							compression = DEFLATE; //Keep checking in case gzip is acceptable
						}
					}
				}
			}
		}
		return compression;
	}

	private String[] getEncodings(String encodingHeader) {
		if (encodingHeader == null)
			return null;
		return encodingHeader.split(",");
	}

	private boolean isEncodingAcceptable(String encoding) {
		int state = STATE_DEFAULT;
		int qvalueIdx = -1;
		for (int i = 0; i < encoding.length(); i++) {
			char c = encoding.charAt(i);
			switch (state) {
			case STATE_DEFAULT: {
				if (';' == c)
					state = STATE_SEPARATOR;
				break;
			}
			case STATE_SEPARATOR: {
				if ('q' == c || 'Q' == c)
					state = STATE_Q;
				break;
			}
			case STATE_Q: {
				if ('=' == c)
					state = STATE_QVALUE;
				break;
			}
			case STATE_QVALUE: {
				if (qvalueIdx < 0 && '0' == c || '1' == c)
					qvalueIdx = i;
				break;
			}
			}
		}

		if (qvalueIdx < 0)
			return true;

		if ("0".equals(encoding.substring(qvalueIdx).trim()))
			return false;
		return true;
	}

	protected CompressedResponseWrapper createWrappedResponse(HttpServletRequest request, HttpServletResponse response,
			final String compressionType) {
		CompressedResponseWrapper wrappedResponse = null;
		if (compressionType.equals(GZIP)) {
			wrappedResponse = new CompressedResponseWrapper(request, response) {
				@Override
				protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,
						HttpServletResponse response, long contentLength, int bufferSize, int minCompressSize)
						throws IOException {
					return new AbstractCompressedStream(compressionType, request, response, contentLength, bufferSize,
							minCompressSize) {
						@Override
						protected DeflaterOutputStream createStream() throws IOException {
							String cacheKey = (String) _request.getAttribute("_TYLER_CACHE_");
							if (cacheKey != null) {
								return new GZIPOutputStream(new CacheServletOutputStream(_response.getOutputStream(),
										cacheKey), _bufferSize);
							} else {
								return new GZIPOutputStream(_response.getOutputStream(), _bufferSize);
							}
						}
					};
				}
			};
		} else if (compressionType.equals(DEFLATE)) {
			wrappedResponse = new CompressedResponseWrapper(request, response) {
				@Override
				protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,
						HttpServletResponse response, long contentLength, int bufferSize, int minCompressSize)
						throws IOException {
					return new AbstractCompressedStream(compressionType, request, response, contentLength, bufferSize,
							minCompressSize) {
						@Override
						protected DeflaterOutputStream createStream() throws IOException {
							String cacheKey = (String) _request.getAttribute("_TYLER_CACHE_");
							if (cacheKey != null) {
								return new DeflaterOutputStream(new CacheServletOutputStream(_response
										.getOutputStream(), cacheKey), new Deflater(_deflateCompressionLevel,
										_deflateNoWrap));
							} else {
								return new DeflaterOutputStream(_response.getOutputStream(), new Deflater(
										_deflateCompressionLevel, _deflateNoWrap));
							}
						}
					};
				}
			};
		} else {
			throw new IllegalStateException(compressionType + " not supported");
		}
		configureWrappedResponse(wrappedResponse);
		return wrappedResponse;
	}

	protected void configureWrappedResponse(CompressedResponseWrapper wrappedResponse) {
		wrappedResponse.setMimeTypes(_mimeTypes);
		wrappedResponse.setBufferSize(_bufferSize);
		wrappedResponse.setMinCompressSize(_minGzipSize);
	}

	private class ContinuationListenerWaitingForWrappedResponseToFinish implements ContinuationListener {

		private CompressedResponseWrapper wrappedResponse;

		public ContinuationListenerWaitingForWrappedResponseToFinish(CompressedResponseWrapper wrappedResponse) {
			this.wrappedResponse = wrappedResponse;
		}

		public void onComplete(Continuation continuation) {
			try {
				wrappedResponse.finish();
			} catch (IOException e) {
				LOG.warn(e);
			}
		}

		public void onTimeout(Continuation continuation) {
		}
	}

	/**
	 * Checks to see if the userAgent is excluded
	 * 
	 * @param ua
	 *            the user agent
	 * @return boolean true if excluded
	 */
	private boolean isExcludedAgent(String ua) {
		if (ua == null)
			return false;

		if (_excludedAgents != null) {
			if (_excludedAgents.contains(ua)) {
				return true;
			}
		}
		if (_excludedAgentPatterns != null) {
			for (Pattern pattern : _excludedAgentPatterns) {
				if (pattern.matcher(ua).matches()) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks to see if the path is excluded
	 * 
	 * @param requestURI
	 *            the request uri
	 * @return boolean true if excluded
	 */
	private boolean isExcludedPath(String requestURI) {
		if (requestURI == null)
			return false;
		if (_excludedPaths != null) {
			for (String excludedPath : _excludedPaths) {
				if (requestURI.startsWith(excludedPath)) {
					return true;
				}
			}
		}
		if (_excludedPathPatterns != null) {
			for (Pattern pattern : _excludedPathPatterns) {
				if (pattern.matcher(requestURI).matches()) {
					return true;
				}
			}
		}
		return false;
	}

	class CacheServletOutputStream extends ServletOutputStream {

		ServletOutputStream delegate;
		ByteArrayOutputStream cache;
		String cacheKey;

		CacheServletOutputStream(ServletOutputStream out, String cacheKey) {
			delegate = out;
			cache = new ByteArrayOutputStream(4096);
			this.cacheKey = cacheKey;
		}

		public ByteArrayOutputStream getBuffer() {
			return cache;
		}

		public void write(int b) throws IOException {
			delegate.write(b);
			cache.write(b);
		}

		public void write(byte b[]) throws IOException {
			delegate.write(b);
			cache.write(b);
		}

		public void write(byte buf[], int offset, int len) throws IOException {
			delegate.write(buf, offset, len);
			cache.write(buf, offset, len);
		}

		public void flush() throws IOException {
			delegate.flush();
		}

		public void close() throws IOException {
			delegate.close();
			CacheKit.set(cacheKey, cache.toByteArray(), 3600 * 24);
			System.out.println("====STORE CAHCE====" + cacheKey);
		}
	}
}
